Externalize default lifecycle plugin versions to POM properties - #13080
Externalize default lifecycle plugin versions to POM properties#13080gnodet wants to merge 2 commits into
Conversation
Move the 13 hardcoded plugin version constants from Java source into POM properties in impl/maven-core/pom.xml. The values are filtered at build time into plugin-versions.properties and loaded at runtime by a new PluginVersions utility class. This makes the default lifecycle plugin versions visible to dependency-update bots (Dependabot, Renovate) that scan POM files for version properties, enabling automated version bump PRs. No behavioral change: all version values are identical to the previous hardcoded constants.
gnodet
left a comment
There was a problem hiding this comment.
Three issues to fix before merge: a silent-failure mode when resource filtering is not applied, a public API surface that doesn't need to be public, and a milestone mismatch.
This review was generated by an AI agent, Hermès, on behalf of @gnodet.
| public static String version(String pluginArtifactId) { | ||
| String key = pluginArtifactId + ".version"; | ||
| String version = VERSIONS.getProperty(key); | ||
| if (version == null) { | ||
| throw new IllegalArgumentException("No default version defined for " + pluginArtifactId + "; add " + key | ||
| + " to plugin-versions.properties"); | ||
| } | ||
| return version; | ||
| } |
There was a problem hiding this comment.
VERSIONS.getProperty(key) returns "${version.maven-clean-plugin}" (not null) when resource filtering is skipped — e.g. when the class is loaded from an IDE or test classpath built without the Maven resources plugin running. The null guard does not catch this: the constant is set to a literal ${…} string, Maven silently tries to resolve a plugin at that version, and the user gets a cryptic "not found in repository" error at build time with no clue that the properties file was never filtered.
Add a format guard that fails fast at class-init time:
| public static String version(String pluginArtifactId) { | |
| String key = pluginArtifactId + ".version"; | |
| String version = VERSIONS.getProperty(key); | |
| if (version == null) { | |
| throw new IllegalArgumentException("No default version defined for " + pluginArtifactId + "; add " + key | |
| + " to plugin-versions.properties"); | |
| } | |
| return version; | |
| } | |
| public static String version(String pluginArtifactId) { | |
| String key = pluginArtifactId + ".version"; | |
| String version = VERSIONS.getProperty(key); | |
| if (version == null) { | |
| throw new IllegalArgumentException("No default version defined for " + pluginArtifactId + "; add " + key | |
| + " to plugin-versions.properties"); | |
| } | |
| if (version.startsWith("${")) { | |
| throw new ExceptionInInitializerError( | |
| "plugin-versions.properties was not filtered at build time; " | |
| + key + " still contains placeholder: " + version); | |
| } | |
| return version; | |
| } |
| * @return the version string, never {@code null} | ||
| * @throws IllegalArgumentException if the plugin is not listed in the properties file | ||
| */ | ||
| public static String version(String pluginArtifactId) { |
There was a problem hiding this comment.
🔸 Unnecessary public API surface
version(String) is public but its only callers are the 13 constants in this same class (all called during class initialisation). Once the constants exist, nothing outside this class needs to call version() at runtime — the public constants are the intended API. Exposing the method invites callers to store the result in their own fields, bypassing future caching or validation improvements.
Make it private:
| public static String version(String pluginArtifactId) { | |
| private static String version(String pluginArtifactId) { |
If external code genuinely needs to look up an arbitrary plugin version, that can be added as a separate, explicitly-documented public method later (with a stronger contract).
| public static final String CLEAN = version("maven-clean-plugin"); | ||
| public static final String COMPILER = version("maven-compiler-plugin"); | ||
| public static final String DEPLOY = version("maven-deploy-plugin"); | ||
| public static final String EAR = version("maven-ear-plugin"); | ||
| public static final String EJB = version("maven-ejb-plugin"); | ||
| public static final String INSTALL = version("maven-install-plugin"); | ||
| public static final String JAR = version("maven-jar-plugin"); | ||
| public static final String PLUGIN = version("maven-plugin-plugin"); | ||
| public static final String RAR = version("maven-rar-plugin"); | ||
| public static final String RESOURCES = version("maven-resources-plugin"); | ||
| public static final String SITE = version("maven-site-plugin"); | ||
| public static final String SUREFIRE = version("maven-surefire-plugin"); | ||
| public static final String WAR = version("maven-war-plugin"); | ||
| } |
There was a problem hiding this comment.
💡 No test for the loading mechanism
The static initialiser, the filtering round-trip, and the null/placeholder guards are the critical path of this new class, yet there is no unit test. A minimal test verifying that every constant is non-null and does not look like an unfiltered placeholder (!CLEAN.startsWith("${")) would catch the filtering-skipped scenario and guard against future regressions (e.g. a new constant added to the class but forgotten in the properties file).
Example:
@Test
void pluginVersionsAreResolved() {
// Verify all constants are loaded and not unfiltered placeholders
for (Field f : PluginVersions.class.getFields()) {
if (f.getType() == String.class) {
String value = (String) f.get(null);
assertNotNull(value, f.getName() + " is null");
assertFalse(value.startsWith("${"), f.getName() + " is unfiltered: " + value);
}
}
}|
Milestone mismatch: the PR targets This comment was generated by an AI agent, Hermès, on behalf of @gnodet. |
| public abstract class AbstractLifecycleMappingProvider implements Provider<LifecycleMapping> { | ||
| // START SNIPPET: versions | ||
| protected static final String RESOURCES_PLUGIN_VERSION = "3.3.1"; | ||
| protected static final String RESOURCES_PLUGIN_VERSION = PluginVersions.RESOURCES; |
There was a problem hiding this comment.
Maybe deprecate these fields, or just remove them if this is all new in 4.0.lx
There was a problem hiding this comment.
Agreed — since these fields are protected and could be referenced by extensions subclassing AbstractLifecycleMappingProvider, I'll deprecate them for 4.1.0 with @Deprecated(since = "4.1.0", forRemoval = true) rather than removing outright. They already delegate to PluginVersions.* constants, so the deprecation is purely a signal to migrate.
This comment was generated by an AI agent, Hermès, on behalf of @gnodet.
ascheman
left a comment
There was a problem hiding this comment.
Thanks @gnodet — I'm on board with the goal (getting the default lifecycle plugin versions out of hardcoded Java constants into something maintainable and bot-updatable), and the mechanism works with CI green. But I'd like to see a rework before it lands, mainly around duplication and whether it actually delivers the Dependabot goal.
1. The version list is now triplicated. Each plugin version lives in three places that must stay in sync:
impl/maven-core/pom.xml→<version.maven-clean-plugin>3.4.0</version.maven-clean-plugin>plugin-versions.properties→maven-clean-plugin.version=${version.maven-clean-plugin}PluginVersions.java→public static final String CLEAN = version("maven-clean-plugin");
Previously each version lived in exactly one place (the constant). Now adding or changing a plugin means touching all three, and it's easy to update the POM but forget the properties file or the constant. The POM property (for the bots) and the filtered .properties (to carry the value to runtime) are both necessary — but the 13 hand-written constants are redundant: callers could use PluginVersions.version("maven-clean-plugin") directly (or a small enum keyed by artifactId), giving the plugin list a single source of truth and dropping the most error-prone layer.
2. Will Dependabot actually bump these? The motivation is bot-visibility, but the new version.* properties aren't referenced by any <dependency>/<plugin> in the reactor — only by resource filtering. Dependabot's Maven ecosystem bumps property-driven versions of declared dependencies/plugins; it doesn't track free-standing version.* properties. Have you confirmed Dependabot (or Renovate) picks these up as-is? If not, we'd also need to declare these plugins (e.g. a <pluginManagement> block referencing the properties) so the bot has something to attach the bump to — otherwise this adds indirection without the automation it's meant to provide.
3. No test guards the filtering. If resource filtering ever regresses, plugin-versions.properties would ship literal ${version.maven-clean-plugin} and every default lifecycle binding would break at runtime with an invalid coordinate. A small unit test asserting every key resolves to a non-${ value would catch that.
Nit: key order differs between the POM (version.maven-clean-plugin) and the file (maven-clean-plugin.version) — harmless, but worth aligning.
Happy with the direction — I'd just like the triplication reduced and the Dependabot path confirmed before it goes in.
|
Thanks @ascheman for the thorough review, and @elharo for the inline suggestion. Let me address each point: 1. Triplication I hear the concern, but I think it's somewhat overstated. The three "copies" aren't really independent sources of truth — they're a pipeline:
Dropping the constants in favor of raw That said, I agree the error-prone scenario (adding a plugin to the POM but forgetting the .properties or the constant) should be guarded. The unit test suggested in the earlier review (and your point 3) covers exactly that. 2. Dependabot / Renovate effectiveness This is the strongest point, and you're right to flag it. Free-standing POM properties not referenced by any Two options to fix this:
I'll go with (a) in the next push. 3. Test Agreed — this was already raised in the initial review. I'll add a unit test that reflectively checks all Nit (key naming) Good catch. I'll align: @elharo's suggestion (deprecate/remove fields) Since Next push will address: placeholder guard, This comment was generated by an AI agent, Hermès, on behalf of @gnodet. |
- PluginVersions.version(): make private, add placeholder guard - plugin-versions.properties: align key naming with POM properties - AbstractLifecycleMappingProvider: deprecate version fields - pom.xml: add pluginManagement for bot visibility (Dependabot/Renovate) - Add PluginVersionsTest: verify all constants are resolved
|
All review feedback addressed in f0bc8d2:
This comment was generated by an AI agent, Hermès, on behalf of @gnodet. |
Move the 13 hardcoded plugin version constants from Java source into POM properties in
impl/maven-core/pom.xml. The values are filtered at build time intoplugin-versions.propertiesand loaded at runtime by a newPluginVersionsutility class.This makes the default lifecycle plugin versions visible to dependency-update bots (Dependabot, Renovate) that scan POM files for version properties, enabling automated version bump PRs.
Changes:
impl/maven-core/pom.xml— Add<properties>section withversion.maven-<name>-pluginentries for all 13 default pluginsplugin-versions.properties— New resource file with${...}placeholders, filtered at build timePluginVersions.java— New utility class that loads versions from the properties file and exposes them as constantsAbstractLifecycleMappingProvider.java— Replace hardcoded version strings withPluginVersions.*constantsDefaultLifecycleRegistry.java— Replace hardcoded clean/site plugin versions withPluginVersions.*constantsNo behavioral change: all version values are identical to the previous hardcoded constants.
Follow-up to the discussion in #13076 about automating plugin version bumps.